#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#
#       estrella.py
#       
#       Copyright 2011 Cleve <mauricio.cleveland@gmail.com>
#       
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#       
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#       
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.
#       
#       

from operator import itemgetter
import threading
import random
import time
import itertools
from nucleo import Nucleo
import copy
import carga_archivo
import numpy

#Defino prec de números en numpy.
numpy.set_printoptions(precision=2)

class Estrella:
  def __init__(self, nucleos, m_distancia, coordenadas, nodos_coord, prob_fusion):
    #threading.Thread.__init__(self)
    
    #Vida estrella
    self.vida = True
    
    #Probabilidad de fusion.
    self.prob_fusion = prob_fusion
    
    #Indico nivel de fusiones
    self.masa = 1
    
    #Diccionario de coordenadas originales (ej: coor[0]=(22,22)).
    self.nucleos_coord = nodos_coord
    
    #Nucleos disponibles
    self.nucleos = range(nucleos)
    
    #Matriz de distancia
    self.m_distancia = m_distancia
    
    #Matriz de distancia original usada para calcular distancia final.
    self.m_distancia_original = copy.deepcopy(m_distancia)
    
    #Coordenadas
    self.coordenadas = coordenadas
    
    #Lista.
    self.coord_fusiones = []
    
    #Lista de nucleos actuales.
    self.ListaNucleos = []
    self.ListaNucleosPermamente =[]
    
    #Convertir a nucleos.
    self.Entrada2Nucleos()
    
    #Control de limite promedio.
    self.CalcularPromedio = True
    
    
  def Entrada2Nucleos(self):
    """ Function doc
  
    @param PARAM: DESCRIPTION
    @return RETURN: DESCRIPTION
    """
  
    for nucleo_aislado in self.nucleos:
      nuevas_coord = self.coordenadas[nucleo_aislado]
      enlace = (nucleo_aislado,)
      identificador = nucleo_aislado
      
      nucleo_nuevo = Nucleo(identificador, nuevas_coord, [], enlace, [])
      self.ListaNucleos.append(nucleo_nuevo)
    
    #Copio la lista de nucleo para su posterior acceso.
    self.ListaNucleosPermamente = copy.deepcopy(self.ListaNucleos)
      
    
  def Distancia_total(self, ruta_encontrada):
    """ Una vez finalizada las fusiones, se procede a calcular la distancia real
    con la matriz de distancia original.
  
    @return RETURN: Menor distancia y conjunto de nodos.
    """
    #Combinaciones de extremos en enlaces internos de nucleos finales.
    combinaciones = itertools.permutations(ruta_encontrada, len(ruta_encontrada))
    
    #almaceno las sumas para luego elegir la mejor.
    sumas = []
    
    for combinacion in combinaciones:
      combinacion_parcial = list(combinacion)
      
      #Lista para almacenar el candidato.
      candidato = []
      
      #Combino los nucleos finales para obtener los enlaces originales.
      for largo_candidato in combinacion_parcial:
        candidato = candidato + largo_candidato.enlaces
      
      #Almaceno la suma.
      suma = 0
      
      #Realizo la suma.
      for indice in xrange(len(candidato)):
        
        if indice < len(candidato)-1:
          
          suma = suma + self.m_distancia_original[candidato[indice]][candidato[indice+1]]
                    
        else:
          
          suma = suma + self.m_distancia_original[candidato[0]][candidato[indice]]
          
      sumas.append((suma, candidato))
    
    #Mejor resultado
    return sorted(sumas)[0]
    
  
  def Distancia_optima(self):
    """ Calcular distancia optima relativa cuando los nucleos son pocos
  
    @return optimo: Ruta optima.
    """
    combinaciones = itertools.permutations(self.ListaNucleos,len(self.ListaNucleos))
    rutas_parciales = []
    ruta_final = []
    
    for combinacion in combinaciones:
      combinacion_parcial = list(combinacion)
      
      suma=0
      
      for i in xrange(len(combinacion_parcial)):
        if i < len(combinacion_parcial)-1:
          suma = self.m_distancia[combinacion_parcial[i].identificador][combinacion_parcial[i+1].identificador] + suma
          
        else:
          suma = self.m_distancia[combinacion_parcial[i].identificador][combinacion_parcial[0].identificador] + suma
          rutas_parciales.append((combinacion_parcial, suma))
      
    #Selecciono mejor ruta segun distancia.
    rutas_parciales = sorted(rutas_parciales, key=itemgetter(1))[0][0]
    
    self.Vida = False
    return rutas_parciales
      
  
  def Fusion(self, enlace):
    """ Function doc
  
    @param enlace: (distancia,(arista0, arista1))
    @return RETURN: coordenadas nuevas, enlace
    """
        
    #Buscar identificador de enlace en ListaNucleosPermamente para fusionar
    for nucleo in self.ListaNucleosPermamente:
      #print 'Identificadores: ', nucleo.identificador, 'masa: ',nucleo.masa
      if nucleo.identificador == enlace[1][0]:
        nucleo_tempo_1 = nucleo
        
      if nucleo.identificador == enlace[1][1]:
        nucleo_tempo_2 = nucleo
          
    nucleo_1 = nucleo_tempo_1
    nucleo_2 = nucleo_tempo_2
    #Fin buscar identificador de enlace en ListaNucleosPermamente para fusionar
    
    
    #Obtengo coordenadas de particulas que colisionan
    coord_x1 = self.nucleos_coord[enlace[1][0]][0]
    coord_x2 = self.nucleos_coord[enlace[1][1]][0]
    coord_y1 = self.nucleos_coord[enlace[1][0]][1]
    coord_y2 = self.nucleos_coord[enlace[1][1]][1]
    #Fin obtengo coordenadas de particulas que colisionan
    
    #Calculo nuevas coordenadas.
    nueva_coord_nucleo_x = (coord_x1 + coord_x2)/2
    nueva_coord_nucleo_y = (coord_y1 + coord_y2)/2
    
    coordenadas_actuales = (nueva_coord_nucleo_x, nueva_coord_nucleo_y)
    #Fin calculo nuevas coordenadas.
    
    #Creo nuevo nucleo.
    nuevo_nucleo = Nucleo(self.identificador, coordenadas_actuales, self.m_distancia_original, nucleo_1.enlaces,nucleo_2.enlaces)
    self.identificador+=1
    
    return nuevo_nucleo
    
  
  def Confinamiento(self):
    """ Function doc
  
    @param PARAM: DESCRIPTION
    @return nucleo: atomos fusionados
    """
    
    #Mientras viva la estrella.
    while(self.vida == True):
      #Matriz de probabilidades que relaciona temperatura en una posicion del confinamiento.
      dimension = numpy.array(self.m_distancia).shape
      
      #Matriz enlaces (MeV).
      m_mev = numpy.random.normal(200,70,dimension)
      
      #Matriz prob de fusion.
      m_prob = numpy.random.normal(6,2,dimension)
      
      #Reinicio identificador.
      self.identificador = 0
    
      #Lista temporal.
      ListaNucleosTemp = []
      #Mientras existan nucleos, fusionar
      
      #Capturo las distancias mas elevadas en la matriz y la menor de ellas es la cota.
      if self.CalcularPromedio == True:
        maxima_distancia = int(numpy.average(numpy.array(self.m_distancia)))
        #maxima_distancia = 6
        
      else:
        maxima_distancia+=1
        
      while(len(self.ListaNucleos)>1):
        
        #Selecciono nucleo aleatoriamente y lo elimino de la lista.
        nucleo_inicial = random.choice(self.ListaNucleos)
        self.ListaNucleos.remove(nucleo_inicial)
        
        distancia_local = []
        
        #Selecciono nucleos que esten disponibles.
        nucleos_disponibles = []
        
        for revision_nucleos in self.ListaNucleos:
          nucleos_disponibles.append(revision_nucleos.identificador)
        
        #Fin selecciono nucleos que esten disponibles.
        for arista in [(nucleo_inicial.identificador, i) for i in nucleos_disponibles if i != nucleo_inicial.identificador]:
          #calculo todas las distancias desde el nucleo
          distancia_local.append((self.m_distancia[arista[0]][arista[1]], arista)) 
          
        #Ordeno distancias (menor a mayor).
        distancia_local = sorted(distancia_local)
        
        #Distancia mas corta ej(dist,(arista0,arista1)).
        mejor_distancia_local = distancia_local[0]
        
        #Si la distancia menor es alguna de las mayores (almacenadas previamente), no realizo enlace.
        if (mejor_distancia_local[0] >= maxima_distancia):
          self.CalcularPromedio = False
          
          nucleo_inicial.identificador = self.identificador
          nucleo_inicial.masa+=1 
          self.identificador+=1
          ListaNucleosTemp.append(nucleo_inicial)
          
        else:
          
          self.CalcularPromedio = True
          
          #Elimino nucleo (mejor arista destino).
          for nucleo_a_borrar in self.ListaNucleos:
            if nucleo_a_borrar.identificador == mejor_distancia_local[1][1]:
              borrar_tempo = nucleo_a_borrar
              break
          
          self.ListaNucleos.remove(borrar_tempo)
          
          #Creo nuevo nucleo con la fusion.
          nuevo_nucleo = self.Fusion(mejor_distancia_local)
          
          #Guardo fusiones.
          ListaNucleosTemp.append(nuevo_nucleo)
        
      #Si quedan nucleos los guardo para una nueva iteracion.
      ListaNucleosTemp = ListaNucleosTemp + self.ListaNucleos
      
      # ---Creo tuplas con coordenadas para calcular distancia ---.
      self.ListaNucleos = copy.deepcopy(ListaNucleosTemp)
      
      coord_xy = []
      
      #Asigno identificadores nuevos para los nuevo nucleos.
      ident=0
      for nucleos in self.ListaNucleos:
        nucleos.identificador = ident
        ident+=1
          
        coord_xy.append(nucleos.coordenadas)
      # ---Fin creo tuplas con coordenadas para calcular distancia ---.
      
      #Copio lista de nucleos.
      self.ListaNucleosPermamente = copy.deepcopy(self.ListaNucleos)
      
      #Calcular matriz de distancia para los nuevos nucleos.
      d, self.m_distancia = carga_archivo.Construye_matriz_distancia(coord_xy)
              
      #Si los nucleos alcanzan una fusion limite se busca la ruta optima.(El problema es tratable)
      if len(self.ListaNucleos) <= 2:
        #Busca menor distancia con coordenadas de fusion
        ruta_encontrada = self.Distancia_optima()
        
        #Busca menor distancia con coordenadas originales
        DistanciaEnlaces = self.Distancia_total(ruta_encontrada)
        
        return DistanciaEnlaces
        break
    
      
      
